#include <sys/param.h>
#include <sys/queue.h>

#include <kore/kore.h>
#include <kore/http.h>
#include <mysql/mysql.h>

#include "kore_mysql.h"

// /* Available buffer types (for parameters binding) */

// typedef enum
// {
//   DB_TYPE_NONE,
//   DB_TYPE_TINYINT,
//   DB_TYPE_SMALLINT,
//   DB_TYPE_INT,
//   DB_TYPE_BIGINT,
//   DB_TYPE_FLOAT,
//   DB_TYPE_DOUBLE,
//   DB_TYPE_TIME,
//   DB_TYPE_DATE,
//   DB_TYPE_DATETIME,
//   DB_TYPE_TIMESTAMP,
//   DB_TYPE_CHAR,
//   DB_TYPE_VARCHAR
// } db_bind_type_t;

/* Structure used to bind data for prepared statements */

// typedef struct
// {
//   db_bind_type_t   type;
//   void             *buffer;
//   unsigned long    *data_len;
//   unsigned long    max_len;
//   char             *is_null;
// } db_bind_t;

// /* Row value definition */

// typedef struct {
//   uint32_t        len;         /* Value length */
//   const char      *ptr;        /* Value string */
// } db_value_t;

// /* Result set row definition */

// typedef struct db_row
// {
//   void            *ptr;        /* Driver-specific row data */
//   db_value_t      *values;     /* Array of column values */
// } db_row_t;

// /* Result set definition */

// typedef struct db_result
// {
//   sb_counter_type_t counter;     /* Statistical counter type */
//   uint32_t       nrows;         /* Number of affected rows */
//   uint32_t       nfields;       /* Number of fields */
//   struct db_stmt *statement;    /* Pointer to prepared statement (if used) */
//   void           *ptr;          /* Pointer to driver-specific data */
//   db_row_t       row;           /* Last fetched row */
// } db_result_t;


// typedef struct db_stmt
// {
//   db_conn_t       *connection;     /* Connection which this statement belongs to */
//   char            *query;          /* Query string for emulated PS */
//   db_bind_t       *bound_param;    /* Array of bound parameters for emulated PS */
//   unsigned int    bound_param_len; /* Length of the bound_param array */
//   db_bind_t       *bound_res;      /* Array of bound results for emulated PS */ 
//   db_bind_t       *bound_res_len;  /* Length of the bound_res array */
//   char            emulated;        /* Should this statement be emulated? */
//   sb_counter_type_t  counter;       /* Query type */
//   void            *ptr;            /* Pointer to driver-specific data structure */
// } db_stmt_t;

struct mysql_job {
  struct http_request *req;
  struct kore_mysql *mysql;

  TAILQ_ENTRY(mysql_job)  list;
};

struct mysql_wait {
  struct http_request   *req;
  struct kore_mysql   *mysql;

  TAILQ_ENTRY(mysql_wait)   list;
};

#define MYSQL_CONN_MAX    2
#define MYSQL_CONN_FREE   0x01
#define MYSQL_LIST_INSERTED 0x0100
// #define MYSQL_QUEUE_LIMIT 1000

static void mysql_queue_wakeup(void);
static void mysql_conn_release(struct kore_mysql *);
static void mysql_set_error(struct kore_mysql *, const char *);
static void mysql_conn_release(struct kore_mysql *);
static void mysql_conn_cleanup(struct mysql_conn *);

static struct mysql_conn  *mysql_conn_create(struct kore_mysql *, struct mysql_db *);
static struct mysql_conn  *mysql_conn_next(struct kore_mysql *, struct mysql_db *);

static struct kore_pool     mysql_job_pool;
static struct kore_pool     mysql_wait_pool;
static TAILQ_HEAD(, mysql_conn)   mysql_conn_free;
static TAILQ_HEAD(, mysql_wait)   mysql_wait_queue;
static LIST_HEAD(, mysql_db)    mysql_db_conn;
static LIST_HEAD(, mysql_db)    mysql_db_conn_strings;

static u_int16_t mysql_conn_count;
u_int32_t mysql_queue_count = 0;
u_int16_t mysql_conn_max = MYSQL_CONN_MAX;
// u_int32_t mysql_queue_limit = MYSQL_QUEUE_LIMIT;

int
kore_mysql_register(const char *name,
  const char *host, 
  const char *dbname, 
  const char *user, 
  const char *passwd, 
  unsigned int port
) {
  struct mysql_db   *mysqldb;

  LIST_FOREACH(mysqldb, &mysql_db_conn_strings, rlist) {
    if (!strcmp(mysqldb->name, name)) {
      return (KORE_RESULT_ERROR);
    }
  }

  mysqldb = kore_malloc(sizeof(*mysqldb));
  mysqldb->name = kore_strdup(name);
  mysqldb->host = kore_strdup(host);
  mysqldb->dbname = kore_strdup(dbname);
  mysqldb->user = kore_strdup(user);
  mysqldb->passwd = kore_strdup(passwd);
  mysqldb->port = port;
  mysqldb->conn_count = 0;
  mysqldb->conn_max = mysql_conn_max;
  // mysqldb->conn_string = kore_strdup(connstring);
  
  LIST_INSERT_HEAD(&mysql_db_conn_strings, mysqldb, rlist);

  return (KORE_RESULT_OK);
}

void
kore_mysql_sys_init(void)
{
  mysql_conn_count = 0;
  TAILQ_INIT(&mysql_conn_free);
  TAILQ_INIT(&mysql_wait_queue);
  LIST_INIT(&mysql_db_conn);

  kore_pool_init(&mysql_job_pool, "mysql_job_pool", sizeof(struct mysql_job), 100);
  kore_pool_init(&mysql_wait_pool, "mysql_wait_pool", sizeof(struct mysql_wait), 100);
}

void
kore_mysql_init(struct kore_mysql *mysql)
{
  memset(mysql, 0, sizeof(*mysql));
  mysql->state = KORE_MYSQL_STATE_INIT;
}

int
kore_mysql_setup(struct kore_mysql *mysql, const char *name)
{
  struct mysql_db   *db;

  db = NULL;

  LIST_FOREACH(db, &mysql_db_conn_strings, rlist) {
    if (!strcmp(db->name, name)) {
      break;
    }
  }

  if (db == NULL) {
    mysql_set_error(mysql, "no database found");
    return (KORE_RESULT_ERROR);
  }

  if ((mysql->conn = mysql_conn_next(mysql, db)) == NULL) {
    return (KORE_RESULT_ERROR);
  }

  return (KORE_RESULT_OK);
}

#if !defined(KORE_NO_HTTP)
void
kore_mysql_bind_request(struct kore_mysql *mysql, struct http_request *req)
{
  if (mysql->req != NULL || mysql->cb != NULL)
    fatal("kore_mysql_bind_request: already bound");

  mysql->req = req;
  mysql->flags |= MYSQL_LIST_INSERTED;

  // LIST_INSERT_HEAD(&(req->mysqls), mysql, rlist);
}
#endif

void
kore_mysql_bind_callback(struct kore_mysql *mysql,
    void (*cb)(struct kore_mysql *, void *), void *arg)
{
  if (mysql->req != NULL)
    fatal("kore_mysql_bind_callback: already bound");

  if (mysql->cb != NULL)
    fatal("kore_mysql_bind_callback: already bound");

  mysql->cb = cb;
  // mysql->arg = arg;
}

int
kore_mysql_query(struct kore_mysql *mysql, const char *query)
{
  if (mysql->conn == NULL) {
    mysql_set_error(mysql, "No connection was set before query.");
    return (KORE_RESULT_ERROR);
  }

  if (mysql_query(mysql->conn->mysql, query) != 0){
    mysql_set_error(mysql, mysql_error(mysql->conn->mysql));
    return (KORE_RESULT_ERROR);
  }
  mysql->result = mysql_store_result(mysql->conn->mysql);

  mysql->state = KORE_MYSQL_STATE_DONE;

  return (KORE_RESULT_OK);
}

int
kore_mysql_get_result(struct kore_mysql *mysql)
{
  MYSQL_ROW row;
  MYSQL_FIELD *field;
  
  unsigned int i;
  unsigned int num_fields;

  num_fields = mysql_num_fields(mysql->result);
  // printf("numbers of result: %d\n", num_fields);

  while (NULL != (field = mysql_fetch_field(mysql->result))) {
    printf("field name: %s\n", field->name);
  }

  while (NULL != (row = mysql_fetch_row(mysql->result))) {
    unsigned long *lengths;
    lengths = mysql_fetch_lengths(mysql->result);

    for (i = 0; i < num_fields; i++) {
        printf("{%.*s} ", (int) lengths[i], row[i] ? row[i] : "NULL");
    }

    printf("\n");
  }

  return (KORE_RESULT_OK);
}

void
kore_mysql_cleanup(struct kore_mysql *mysql)
{
  kore_debug("kore_mysql_cleanup(%p)", mysql);

  if (mysql->result != NULL) {
    mysql_free_result(mysql->result);
  }

  if (mysql->error != NULL) {
    kore_free(mysql->error);
  }

  if (mysql->conn != NULL) {
    mysql_conn_release(mysql);
  }

  mysql->result = NULL;
  mysql->error = NULL;
  mysql->conn = NULL;

  if (mysql->flags & MYSQL_LIST_INSERTED) {
    LIST_REMOVE(mysql, rlist);
    mysql->flags &= ~MYSQL_LIST_INSERTED;
  }
}

void
kore_mysql_logerror(struct kore_mysql *mysql)
{
  kore_log(LOG_NOTICE, "kore_mysql.h, mysql error: %s",
      (mysql->error) ? mysql->error : "unknown");
}

static struct mysql_conn *
mysql_conn_next(struct kore_mysql *mysql, struct mysql_db *db)
{
  struct mysql_conn *conn;

  conn = NULL;

  TAILQ_FOREACH(conn, &mysql_conn_free, list) {
    if (!(conn->flags & MYSQL_CONN_FREE)) {
      fatal("got a mysql connection that was not free?");
    }
    if (!strcmp(conn->name, db->name)) {
      break;
    }
  }

  if (conn == NULL) {
    if (mysql_conn_count >= mysql_conn_max) {
      mysql_set_error(mysql, "no available connection");
      return (NULL);
    }

    // if ((conn->mysql = mysql_init(NULL)) == NULL)
    if ((conn = mysql_conn_create(mysql, db)) == NULL) {
      return (NULL);
    }
  }

  conn->flags &= ~MYSQL_CONN_FREE;
  TAILQ_REMOVE(&mysql_conn_free, conn, list);

  return (conn);
}

static void
mysql_set_error(struct kore_mysql *mysql, const char *msg)
{
  if (mysql->error != NULL) {
    kore_free(mysql->error);
  }

  mysql_close(mysql->conn->mysql);
  mysql_library_end();

  mysql->error = kore_strdup(msg);
  mysql->state = KORE_MYSQL_STATE_ERROR;
}

static void
mysql_queue_wakeup(void)
{
  struct mysql_wait *myw, *next;

  for (myw = TAILQ_FIRST(&mysql_wait_queue); myw != NULL; myw = next) {
    next = TAILQ_NEXT(myw, list);
    if (myw->req->flags & HTTP_REQUEST_DELETE)
      continue;

    http_request_wakeup(myw->req);

    TAILQ_REMOVE(&mysql_wait_queue, myw, list);
    kore_pool_put(&mysql_wait_pool, myw);
    return;
  }
}

static struct mysql_conn *
mysql_conn_create(struct kore_mysql *mysql, struct mysql_db *db)
{
  struct mysql_conn *conn;

  if (db == NULL ||
    db->host == NULL ||
    db->user == NULL ||
    db->passwd == NULL ||
    db->dbname == NULL
  ) {
    if (db->port == 0 || db->unix_socket == NULL) {
      fatal("mysql_conn_create: No connection data.");
    }
  }

  mysql_conn_count++;
  conn = kore_malloc(sizeof(*conn));
  kore_debug("mysql_conn_create(): %p", conn);

  conn->mysql = mysql_init(conn->mysql);
  if (conn->mysql == NULL){
    mysql_set_error(mysql, mysql_error(conn->mysql));
    mysql_conn_cleanup(conn);
    return (NULL);
  }

  if (mysql_real_connect(conn->mysql, db->host, db->user,
    db->passwd, db->dbname, db->port, db->unix_socket,
    db->flags) == NULL) {
    mysql_set_error(mysql, mysql_error(conn->mysql));
    mysql_conn_cleanup(conn);
    return (NULL);
  } 

  conn->job = NULL;
  conn->flags = MYSQL_CONN_FREE;
  conn->name = kore_strdup(db->name);
  TAILQ_INSERT_TAIL(&mysql_conn_free, conn, list);

  return (conn);
}

static void
mysql_conn_release(struct kore_mysql *mysql)
{
  if (mysql->conn == NULL) {
    return;
  }

  /* Drain just in case. */
  while (mysql_use_result(mysql->conn->mysql) != NULL)
    ;

  mysql->conn->job = NULL;
  mysql->conn->flags |= MYSQL_CONN_FREE;
  TAILQ_INSERT_TAIL(&mysql_conn_free, mysql->conn, list);

  mysql->conn = NULL;
  mysql->state = KORE_MYSQL_STATE_COMPLETE;

  mysql_queue_wakeup();
}

static void
mysql_conn_cleanup(struct mysql_conn *conn)
{
  struct http_request *req;
  struct kore_mysql *mysql;

  kore_debug("mysql_conn_cleanup(): %p", conn);

  if (conn->flags & MYSQL_CONN_FREE)
    TAILQ_REMOVE(&mysql_conn_free, conn, list);

  if (conn->job) {
    req = conn->job->req;
    mysql = conn->job->mysql;
    http_request_wakeup(req);

    mysql->conn = NULL;
    mysql_set_error(mysql, mysql_error(conn->mysql));

    kore_pool_put(&mysql_job_pool, conn->job);
    conn->job = NULL;
  }

  if (conn->mysql != NULL) {
    // PQfinish(conn->mysql);
  }

  mysql_conn_count--;
  kore_free(conn->name);
  kore_free(conn);
}
